<?php
/**
 * 每日定时脚本生成器
 * User: XuemingZhang
 * Date: 2018/6/11
 */

namespace app\command;

use app\model\Schedule;
use Swoole\Process;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Cache;
use Swoole\Timer as SwooleTimer;

class Timer extends Command
{

    protected function configure()
    {
        $this->setName('timer')
            ->addArgument('cmd')
            ->setDescription('秒级定时器');
    }

    protected function execute(Input $input, Output $output)
    {
        $cmd = $input->getArgument('cmd');
        if (empty($cmd) || !in_array($cmd, ['start', 'restart', 'stop'])) {
            $cmd = 'start';
        }
        $this->app->invokeMethod([$this, $cmd]);
    }

    /**
     * 启动定时器
     */
    public function start(Output $output): void
    {
        //设置主进程名称
        cli_set_process_title('crontab master');
        //分发子进程
		$this->dispatch();
        //将当前进程转为守护进程
//        Process::daemon();
        $pid = getmypid();
//        SwooleTimer::tick(1000, $this->dispatch());
//        Cache::set('timer_pid', $pid, 0);
        $output->writeln('定时器启动成功,pid:' . $pid);
		//回收子进程
        $this->wait();

    }

    /**
     * 关闭定时器
     */
    public function stop(Output $output): void
    {
        $key = 'timer_id';
        $timer_id = Cache::get($key);
        if (is_null($timer_id)) {
            SwooleTimer::clearAll();
        } else {
            SwooleTimer::clear($timer_id);
        }
        Cache::delete($key);
        $output->writeln('定时器停止成功');
    }

    /**
     * 重启定时器
     */
    public function restart(Output $output): void
    {
        $this->stop($output);
        $this->start($output);
    }

    /**
     * 分发子任务
     */
    private function dispatch(): void
    {

//        return function () {
            $schedule_list = $this->getScheduleList();
            if (empty($schedule_list)) {
                return;
            }
            $timestamp = time();

			$think_path = root_path() . 'think';
            foreach ($schedule_list as $v) {
                //判断参数是否参数合法和执行时间、最大并发
                if (!$this->checkParams($v) || !$this->checkTime($v, $timestamp) || !$this->checkConcurrency($v)) {
                    continue;
                }
                $process = new Process(function (Process $process) use($v,$think_path) {
                    //获取业务参数并过滤空白字符
                    $params = array_filter(explode(' ', $v->cmd), function($item) {
                        return !empty($item);
                    });
                    //公共参数
                    $cmd = [$think_path, 'crontab'];
                    $params = array_merge($cmd, $params);
                    $params[] = 'crontab_code=' . $v->code;
                    $process->exec($this->getPhpBin(), $params);
                });
                $process->start();
//                $this->nohubProcess("{$php_bin} {$think_path} crontab {$v->cmd}");
                $v->last_time = time();
                $v->save();
            }
//        };
    }

    /**
     * 回收子进程
    */
    private function wait() {
//        Process::signal(SIGCHLD, function ($sig) {
            //必须为false，非阻塞模式
            while ($ret = Process::wait()) {
                echo "PID={$ret['pid']}进程结束", PHP_EOL;
            }
//        });
    }

    /**
     * 获取计划任务配置
    */
    private function getScheduleList()
    {
//        $key = 'schedule_list';
//        $schedule_list = Cache::get($key);
//        if (is_null($schedule_list)) {
            $schedule_list = Schedule::where('status', 1)
                ->field('id,code,last_time,cmd,meanwhile_num')
                ->select();
//            Cache::set($key, $schedule_list, 0);
//        }
        return $schedule_list;
    }

    private function getPhpBin()
    {
        $php_bin = PHP_BINDIR . '/php';
        if (!file_exists($php_bin)) {
            exit("[{$php_bin}]不存在,请确认php已经正确安装并且php预定义常量PHP_BINDIR设置正确或者直接定义(define)_PHP_BIN_常量");
        }
        return $php_bin;
    }

    // 检查参数是否合法，防止非法脚本被执行
    private function checkParams($_cron)
    {
        //防止计划任务注入
        if (!preg_match("/^\w+\/\w+\s?(\w+=\w+)?((,|，)\w+=\w+)*$/", $_cron->cmd)) {
            return false;
        }
        return true;
    }

    // 检查是否符合时间触发条件
    private function checkTime(Schedule $_cron, $timestamp)
    {
        if ($_cron->type == 1) {
            $cron_times = explode('|', $_cron->value);
            $time_value = intval(trim($cron_times[0]));

            $cron_time_start = $cron_time_end = 0;
            if (isset($cron_times[1])) {
                $cron_time_spans = explode('-', $cron_times[1]);
                if (count($cron_time_spans) == 2) {
                    $cron_time_start = strtotime(date('Y-m-d ', $timestamp) . trim($cron_time_spans[0]));
                    $cron_time_end = strtotime(date('Y-m-d ', $timestamp) . trim($cron_time_spans[1]));
                }
            }
            //不在可执行时间范围内
            if ($cron_time_start > $cron_time_end) {
                if ($timestamp <= $cron_time_end || $timestamp >= $cron_time_start) {
                    return false;
                }
            } else {
                if ($timestamp >= $cron_time_start && $timestamp <= $cron_time_end) {
                    return false;
                }
            }
            //还没有到执行时间
            if (($timestamp - $_cron->last_time) < ($time_value * 60)) {
                return false;
            }

        } else if ($_cron->type == 2) {
            //每天定点执行 已经执行时间比设置时间小
            $time_value = trim($_cron->value);
            $time_str = date('Y-m-d ', $timestamp) . $time_value . ':00';
            $time_str = strtotime($time_str);

            //还没有到执行时间
            if (abs($time_str - $timestamp) > 30) {
                return false;
            }

        } else if ($_cron->type == 3) {
            //每周定点执行 已经执行时间比设置时间小
            $time_value = trim($_cron->value);

            $week_day = substr($time_value, 0, 1);
            //不是指定可以运行的时间
            if (date('N', $timestamp) != $week_day) {
                return false;
            }

            $time_value = substr($time_value, strlen($time_value) - 5, 5);
            $time_str = date('Y-m-d ', $timestamp) . $time_value . ':00';
            $time_str = strtotime($time_str);

            //还没有到执行时间
            if (abs($time_str - $timestamp) > 30) {
                return false;
            }
        } else if ($_cron->type == 4) {
            //每月定点执行 已经执行时间比设置时间小
            $time_value = trim($_cron->value);

            $month_day = substr($time_value, 0, 2);
            //不是指定可以运行的日期
            if (date('d', $timestamp) != $month_day) {
                return false;
            }

            $time_value = substr($time_value, strlen($time_value) - 5, 5);
            $time_str = date('Y-m-d ', $timestamp) . $time_value . ':00';
            $time_str = strtotime($time_str);

            //还没有到执行时间
            if (abs($time_str - $timestamp) > 30) {
                return false;
            }

        }

        return true;
    }

    private function checkConcurrency(Schedule $_cron) : bool
    {
//        $crontab_dir = root_path();
        //获得正在运行的计划任务的个数
        $filter = 'ps aux|grep "crontab '  . $_cron->code . '"|grep -v "crontab '  . $_cron->code . '"|wc -l';
        $curr_num = intval(exec($filter));
        //如果已经执行了系统允许的最大个数的话,直接退出
        if ($curr_num > $_cron->meanwhile_num - 1) {
            return false;
        }
        return true;
    }

    private function nohubProcess($cmd) {
        $nohub_path = runtime_path() . 'nohub.out';
        return system("nohup {$cmd} >> {$nohub_path} 2>&1 &");
    }
}
